Skip to content

fix(builtins): honor --help argv order and rewrite pflag errors to GNU style#238

Open
julesmcrt wants to merge 7 commits intomainfrom
df-followups
Open

fix(builtins): honor --help argv order and rewrite pflag errors to GNU style#238
julesmcrt wants to merge 7 commits intomainfrom
df-followups

Conversation

@julesmcrt
Copy link
Copy Markdown
Collaborator

Summary

Two follow-ups from PR #205 (df builtin) review feedback:

  1. cmd --help --bogus now prints help and exits 0, matching GNU coreutils' left-to-right --help short-circuit. Previously pflag scanned the whole argv and failed on --bogus first. Implemented in builtins/builtins.go via a pre-parse args trim, scoped to commands that register a help flag.

  2. pflag flag-error wording now matches GNU getopt byte-for-byte:

    • unknown flag: --foounrecognized option '--foo'
    • unknown shorthand flag: 'X' in -...invalid option -- 'X'
    • flag does not allow an argumentoption '--foo' doesn't allow an argument
    • flag needs an argument: --foooption '--foo' requires an argument
    • Try 'cmd --help' for more information. footer appended when the command exposes --help.

Several df scenarios that previously carried skip_assert_against_bash solely because of wording divergence now match GNU byte-for-byte and have been de-skipped. Sibling scenarios and Go tests for ping, pwd, ip, strings, uniq, sed, and du were updated to the new wording.

Refs:

Test plan

  • `go test ./...` passes locally
  • `RSHELL_BASH_TEST=1 go test ./tests/ -run TestShellScenariosAgainstBash` passes (all de-skipped `df` scenarios match GNU byte-for-byte)
  • New unit test `TestRewritePflagError` covers each translation pattern + a pass-through case
  • New scenarios lock in the `--help` short-circuit behavior:
    • `tests/scenarios/cmd/df/basic/help_short_circuits_before_unknown_flag.yaml`
    • `tests/scenarios/cmd/df/errors/unknown_flag_before_help.yaml` (verifies left-to-right semantics still error on a bad flag positioned before `--help`)
  • `make fmt` clean

🤖 Generated with Claude Code

…U style

Two follow-ups from PR #205 (df builtin) review:

1. `cmd --help --bogus` now prints help and exits 0, matching GNU
   coreutils' left-to-right `--help` short-circuit. Previously pflag
   scanned the whole argv and failed on `--bogus` first. Implemented
   in `builtins/builtins.go` via a pre-parse args trim, scoped to
   commands that register a `help` flag.

2. pflag's default flag-error wording (`unknown flag: --foo`,
   `unknown shorthand flag: 'X' in -...`, `flag does not allow an
   argument`, `flag needs an argument: --foo`) is now translated to
   GNU's `unrecognized option '--foo'`, `invalid option -- 'X'`,
   `option '--foo' doesn't allow an argument`, and `option '--foo'
   requires an argument`. A `Try 'cmd --help' for more information.`
   footer is appended when the command exposes `--help`.

Several df scenarios that previously carried `skip_assert_against_bash`
solely because of wording divergence now match GNU byte-for-byte and
have been de-skipped. Updated all sibling scenarios and Go tests
(ping, pwd, ip, strings, uniq, sed, du) that asserted on the old
pflag wording.

Refs:
- #205 (comment)
- #205 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25059712c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
Codex review caught that `df --no-such=value` emitted
`df: unrecognized option '--no-such'` while GNU emits the full token
`'--no-such=value'`. pflag strips the `=value` suffix from its error
string, so `rewritePflagError` now takes the parsed argv and scans it
to recover the original token.

Added a unit-test case (--no-such=value, --no-such=, and `--` boundary)
plus a scenario test that asserts byte-for-byte match against GNU.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c85098992

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go
Codex caught a second wording divergence: pflag emits two different
payloads for a missing argument depending on flag form:

  long form:  `flag needs an argument: --foo`
  short form: `flag needs an argument: 'X' in -Y`

Previously the rewriter inlined the payload into a single template,
producing `option ''n' in -n' requires an argument` for `head -n`.
GNU uses two separate templates:

  long:  `option '--foo' requires an argument`
  short: `option requires an argument -- 'X'`

`rewritePflagError` now branches on the payload shape via a small
`shortMissingArg` helper. Added unit-test cases for the single
shorthand and the combined-shorthand form (`-an`) plus scenario tests
under `tests/scenarios/cmd/head/errors/missing_arg_{short,long}.yaml`
that assert against GNU.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb77a039a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
julesmcrt and others added 2 commits May 7, 2026 16:48
Codex flagged that the previous trim "args at first --help" was too
aggressive: `head -n nope --help --bogus` would discard `--bogus`,
let pflag parse `-n nope --help` cleanly, then short-circuit on
--help and exit 0 — silently swallowing both the unknown later flag
and the invalid earlier value. GNU instead surfaces the earlier
validation error.

Restricted the trim to the case where `--help` is the very first
argument. This still covers the cases the original reviewer cited
(`df --help --sync`, `df --help --bad`) and every existing test, but
keeps full pflag validation when `--help` appears after another
flag — preserving GNU's "fail on the leftmost bad option" semantics.

Added a regression scenario at
`tests/scenarios/cmd/head/errors/help_after_value_does_not_swallow_unknown_flag.yaml`
that locks in: `head -n nope --help --bogus` exits 1 (does not print
help). skip_assert_against_bash is set because GNU's failure reason
(invalid -n value) and rshell's (unknown --bogus) differ — both exit
1 and neither prints help, which is the property we care about. The
underlying value-validation gap is a pre-existing issue out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GNU getopt parses `-h=true` as the no-arg shorthand `-h` followed by
`=` interpreted as another shorthand char (which isn't registered),
emitting `df: invalid option -- '='`. pflag instead treats `=` as the
value separator and routes the value through Set, which our no-arg
guards reject.

Detect the `-X=value` argv shape in the rewriter and substitute GNU's
wording when the descriptor's shorthand char matches X. Closes the
last wording-only divergence flagged in PR #205's follow-up audit
(`tests/scenarios/cmd/df/flags/rejected_h_equals_true.yaml`); skip
removed and stderr now matches GNU byte-for-byte.

Updated unit tests:
- new case: -h=true with descriptor `-h, --human-readable` → `'='`
- existing -k=true case (short-only descriptor) updated — GNU also
  emits `invalid option -- '='` for that, not `option '-k' doesn't
  allow an argument`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f55983930b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
julesmcrt and others added 2 commits May 7, 2026 17:24
Codex flagged that GNU honors --help even after earlier valid options
— `head -q --help --bogus` and `df -h --help --bogus` print help and
exit 0. The previous "first-only" trim was too narrow.

The new `safeHelpTrimIndex` scans tokens before --help and trims only
when every preceding token is a registered no-argument flag (long form
or shorthand cluster). Tokens that could fail validation (value-takers,
=value forms, positionals, unknown flags, end-of-flags `--`) all
preclude the trim — keeping the earlier-failure semantics from the
prior round (`head -n nope --help --bogus` still errors instead of
silently swallowing the invalid value).

Added unit tests for `safeHelpTrimIndex` covering each token class and
a regression scenario at
`tests/scenarios/cmd/df/basic/help_short_circuits_after_no_arg_flag.yaml`
(`df -h --help --bogus` exits 0 with usage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's `Fuzz (pwd)` and `Fuzz (du)` jobs caught a panic in the new
`isNoArgFlagToken`: for a token like `-˞`, the loop calls
`fs.ShorthandLookup(string(a[i]))`. When `a[i]` is a UTF-8
continuation byte (≥ 0x80), `string(byte)` produces the 2-byte UTF-8
encoding of that byte interpreted as a rune (e.g. 0xCB → "Ë"), and
pflag panics with `can not look up shorthand which is more than one
ASCII character`. The interpreter recovers and reports `internal
error` from api.go's panic guard.

Guard the loop: any byte outside printable ASCII (≤ ' ' or > 0x7E)
disqualifies the token as a no-arg cluster — non-ASCII bytes can't
be valid shorthand chars anyway. Captured the failing input as a
permanent corpus seed (`builtins/pwd/testdata/fuzz/FuzzPwdArgs/9386e59311458487`)
and added a unit-test case to TestSafeHelpTrimIndex.

Verified locally: 30s fuzz on both pwd and du now passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@julesmcrt
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18e763ab39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go
Comment on lines +218 to +219
if !isNoArgFlagToken(fs, a) {
return 0, false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor --help after valid value-taking options

For commands with value-taking flags, this rejects trimming as soon as a valid value option appears before --help, so head -n 5 --help --bogus is left for pflag to parse and fails on the later --bogus. GNU head -n 5 --help --bogus exits 0 and prints help because the earlier -n 5 is valid and --help short-circuits before later options; this means the new left-to-right help behavior is only correct for no-arg flags and regresses valid invocations of head/tail-style builtins.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Opus 4.7 (1M context)] You're right that GNU prints help here, but extending the trim to allow value-takers conflicts with the prior round's feedback (3202120242) about head -n nope --help --bogus — pflag accepts nope as a String value but head's numeric validation runs only after --help short-circuits, so trimming would silently swallow the validation failure. GNU itself diverges by value validity:

  • head -n 5 --help --bogus → exit 0, prints help (-n 5 is valid)
  • head -n nope --help --bogus → exit 1, invalid number of lines: 'nope'

Verified both with docker run --rm debian:bookworm-slim. To match GNU on both, head's handler would need to validate -n's numeric value via pflag.Var.Set before the --help short-circuit fires — that's a per-builtin handler-ordering change, not something the shared trim in builtins/builtins.go can resolve safely (we don't know which value-taker fields a builtin's handler validates internally).

Trade-off matrix with the current no-arg-only trim:

invocation GNU rshell now extending trim
-q --help --bogus (no-arg) help, 0 help, 0 ✓ help, 0 ✓
-n 5 --help --bogus help, 0 error, 1 help, 0 ✓
-n nope --help --bogus error, 1 error, 1 ✓ help, 0 ✗

Two out of three match either way; extending makes the -n 5 case match at the cost of regressing the nope case. Deferring to a follow-up PR that re-orders head/tail/etc. handlers to validate values before --help short-circuits, which closes both gaps cleanly. Leaving this thread open for the reviewer to decide between the trade-off.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in another PR

@julesmcrt julesmcrt marked this pull request as ready for review May 7, 2026 15:59
Comment thread builtins/builtins.go
// flag does not allow an argument` an argument"
//
// Unknown messages are returned unchanged.
func rewritePflagError(err error, args []string) string {
Copy link
Copy Markdown
Member

@AlexandreYang AlexandreYang May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum,

1/ I'm not sure if patching pflag is the right way. Maybe we should just re-code our own flag parser. WDYT?

2/ I think we should at least move all the flag parsing logic to a separate package like /builtins/internal/flagparser/ to isolate flag parsing logic. Having all this flag parsing logic in builtins.go seems not ideal.
And then, we can optionally implement 1/ more easy if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants